/**
 * @particulars patch 方法实现虚拟dom 转化为真实dom，同时也包含diff 算法
 * diff 算法，采用双指针的方式对新旧虚拟dom 进行同级比较，边循环边处理
 *  1、新旧虚拟dom 相同，则复用老节点
 *  2、新旧虚拟dom 同级不相同（key 或者是节点标识不一致），则直接替换新的虚拟doom，其子节点不在进行比较
 *  3、对于子节点中，新的虚拟dom 没有，旧的虚拟dom 有，则直接删除对应的老节点
 *  4、对于子节点中，旧的虚拟dom 没有，新的虚拟dom 有，则直接插入新节点
 * 在进行diff 比较的同时，会对dom 打上补丁进行增删改等一系列操作，然后统一更新（这些批量修改页面的操作，浏览器会统一处理进行优化）
 */

import { isSameVnode } from ".";

// 8、创建组件
function createComponent(vnode) {
  let data = vnode.data;
  // 如果有data.hook 和data.hook.init，则调用data.hook.init(vnode) 去创建组件
  if ((data = data.hook) && (data = data.init)) {
    data(vnode);
  }

  // 说明是组件
  if (vnode.componentInstance) {
    return true;
  }

  return false;
}

// 2、根据虚拟dom 创建真实dom
export function createElement(vnode = {}) {
  const { tag, text, data, children } = vnode;

  // 是标签的话，创建标签(自定义的组件标签和html 的原始标签)
  if (typeof tag === 'string') {

    // 创建真实dom，需要区分是组件还是元素 <my-button /> / <h1></h1>
    if (createComponent(vnode)) {
      // 是组件就直接返回真实dom
      return vnode.componentInstance.$el;
    }

    // 否则就创建节点，挂载属性等......
    vnode.el = document.createElement(tag);

    // 4-1、处理标签属性
    patchProps(vnode.el, {}, data);

    // 4-2、对于子节点进行递归处理，一直往根节点里面塞入子节点
    if (children) {
      children.forEach(child => {
        vnode.el.appendChild(createElement(child));
      });
    }
  } else {
    // 否则就是文本，创建对应的文本
    vnode.el = document.createTextNode(text);
  }

  // 返回真实的节点
  return vnode.el;
}

// 3、根据虚拟dom 的元素属性，对对应的节点属性进行处理
export function patchProps(el, oldProps = {}, props = {}) {

  // 3-1、旧虚拟dom 的属性中有，新的没有，则需要删除老的属性
  const oldStyles = oldProps.style || {};
  const newStyles = props.style || {};

  for (let key in oldStyles) {
    if (!newStyles[key]) {
      // 新节点没有该样式，则将老节点样式置空
      el.style[key] = '';
    }
  }

  for (let key in oldProps) {
    if (!props[key]) {
      // 新节点没有该属性，则删除老节点属性
      el.removeAttribute(key);
    }
  }

  // 3-2、新的属性覆盖老的属性
  for (let key in props) {
    // 对于style 属性进行特殊处理
    if (key === 'style') {
      const style = props.style || {};

      for (let styleName in style) {
        el.style[styleName] = style[styleName];
      }
    } else {
      // 否则直接在节点上新增对应的属性，对应组件需要兼容
      el && el.setAttribute && el.setAttribute(key, props[key]);
    }
  }
}

// 1、将虚拟dom 替换成真实dom，既有初始化，也包含diff 更新操作
export function patch(oldVnode, vnode) {

  // 组件的挂载
  if (!oldVnode) {
    return createElement(vnode);
  }

  // 是否为dom 节点，如果是节点则进行初始化替换操作，否则就是被创建的虚拟dom，需要进行diff 运算
  const isRealElement = oldVnode.nodeType;

  if (isRealElement) {
    const el = oldVnode;
    const parentElement = el.parentNode;

    // 1-1、依据虚拟dom 创建真实dom
    const newElement = createElement(vnode);

    // 1-2、先在原基础上新增依据虚拟dom 创建的真实dom
    parentElement.insertBefore(newElement, el.nextSibling);
    // 1-3、再删除之前原有的未处理的真实dom
    parentElement.removeChild(el);

    return newElement;
  } else {
    // diff 算法是一个平级的比较
    // 1、两个节点不是同一个节点，直接删除老节点替换成新的节点，不需要再进行对比
    // 2、两个节点是同一个节点（判断节点的tag 和节点的key），比较两个节点的属性是否有差异（复用相同的，更新差异属性）
    // 3、节点比较完毕后就需要比较两个人的子节点（子文本节点和子标签节点）
    return patchVnode(oldVnode, vnode);
  }
}

// 4、diff 运算，对新旧虚拟dom 进行差异比较，补丁操作
function patchVnode(oldVnode, vnode) {
  // 4-1、不是同一个节点，直接删除替换，返回新节点
  if (!isSameVnode(oldVnode, vnode)) {
    const el = createElement(vnode);
    oldVnode.el.parentNode.replaceChild(el, oldVnode.el);
    return el;
  }

  // 4-2、可能是文本，对文本进行差异比较
  // 复用老节点元素，在老节点上打上补丁，进行差异更新
  const el = vnode.el = oldVnode.el;
  if (!oldVnode.tag) {
    if (oldVnode.text !== vnode.text) {
      el.textContent = vnode.text;
    }
  }

  // 4-3、是标签，我们需要比对标签的属性
  patchProps(el, oldVnode.data, vnode.data);

  // 4-4、比较子节点，一方有子节点，一方没有；两方都有
  const oldChildren = oldVnode.children || [];
  const newChildren = vnode.children || [];

  if (oldChildren.length > 0 && newChildren.length > 0) {
    // 新旧虚拟dom 两个都有子节点，完整的diff 算法，双指针比较
    updateChildren(el, oldChildren, newChildren);
  } else if (newChildren.length > 0) {
    // 新虚拟dom 有子节点，老的没有，直接插入新节点
    mountChildren(el, newChildren);
  } else if (oldChildren.length > 0) {
    // 老虚拟dom 有子节点，但新的没有，直接删除，el.innerHTML = ''
    unMountChildren(oldChildren);
  }

  return el;
}

// 5、插入新增的新节点
function mountChildren(el, newChildren) {
  for (let i = 0; i < newChildren.length; i++) {
    const child = newChildren[i];
    el.appendChild(createElement(child));
  }
}

// 6、删除多余的旧节点
function unMountChildren(newChildren) {
  for (let i = 0; i < newChildren.length; i++) {
    const child = newChildren[i] || {};
    const el = child.el;

    if (child.tag) {
      // 元素节点删除
      el && el.parentNode.removeChild(el);
    } else {
      // 文本字节置空
      el && (el.textContent = '');
    }
  }
}

// 7、diff 过程中，对节点打上补丁，操作dom，进行差异化更新
function updateChildren(el, oldChildren, newChildren) {
  // 新老节点开始的索引，从左往右的指针
  let oldStartIndex = 0;
  let newStartIndex = 0;

  // 新老节点末尾的索引，从右往左的指针
  let oldEndIndex = oldChildren.length - 1;
  let newEndIndex = newChildren.length - 1;

  // 第一个子元素
  let oldStartVnode = oldChildren[0];
  let newStartVnode = newChildren[0];

  // 最后一个子元素
  let oldEndVnode = oldChildren[oldEndIndex];
  let newEndVnode = newChildren[newEndIndex];

  // 乱序操作，对于旧虚拟dom 的子元素进行映射操作，便于比较更新
  function makeIndexBykey(children) {
    const map = {};
    children.forEach((child, index) => map[child.key] = index);
    return map;
  }
  const map = makeIndexBykey(oldChildren);

  // 7-1、双方有一头的指针大于头部的指针则停止循环，如果是相同节点则递归进行比较
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (!oldStartVnode) {
      // 针对于乱序比较后的置空跳过，老节点头部为空，则将头指针往后移动
      oldStartVnode = oldChildren[++oldStartIndex];
    } else if (!oldEndVnode) {
      // 针对于乱序比较后的置空跳过，老节点尾部为空，则将尾指针往前移动
      oldEndVnode = oldChildren[--oldEndIndex];
    } else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 老节点和新节点第一个节点一致，则从头开始比较，同时将新旧头指针往后开始移动，abc => abcd
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIndex];
      newStartVnode = newChildren[++newStartIndex];
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 老节点和新节点最后的节点一致，则从尾部开始比较，同时将新旧尾指针往前开始移动，dabc => abc
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIndex];
      newEndVnode = newChildren[--newEndIndex];
    } else if (isSameVnode(oldEndVnode, newStartVnode)) {
      // 老节点和新节点前后不一致，则进行交叉比较，头尾互相比较，比对上了，就移到最后，同时将旧节点头指针往后移，新节点尾指针往前移，abcd => dcba
      patchVnode(oldEndVnode, newStartVnode);
      // 比较完将老节点的尾巴移动到老节点的前面去
      el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
      oldStartVnode = oldChildren[++oldStartIndex];
      newEndVnode = newChildren[--newEndIndex];
    } else {
      // 如果都匹配不上，则进行乱序比较，根据老的虚拟dom 生成映射列表，对新的虚拟dom 进行查找，找到则移动，找不到则添加，多余的就进行删除，同时将旧节点头指针往后移动，abcd => badcfg
      const moveIndex = map[newStartVnode.key];
      if (moveIndex !== undefined) {
        // 找到对应的节点进行复用，并移动到老节点的前面
        const moveVnode = oldChildren[moveIndex];
        el.insertBefore(moveVnode.el, oldStartVnode.el);
        // 标识这个节点已经被移动走了，需要跳过
        oldChildren[moveIndex] = undefined;
        // 同时比对属性和子节点
        patchVnode(moveVnode, newStartVnode);
      } else {
        // 在列表中没有找到，则进行插入
        el.insertBefore(createElement(newStartVnode), oldStartVnode.el);
      }
      newStartVnode = newChildren[++newStartIndex];
    }
  }

  // 7-2、新节点多了，多余的就插入
  if (newStartIndex <= newEndIndex) {
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      const childElement = createElement(newChildren[i]);
      // 取下一个节点作为参照物，有可能是向前追加，也可能是往后追加
      const nextAnchorNode = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null;
      // nextAnchorNode 为null 的时候则会认为是appendChild，否则就是向前追加
      el.insertBefore(childElement, nextAnchorNode);
    }
  }

  // 7-3、老节点多了，就直接删除
  if (oldStartIndex <= oldEndIndex) {
    for (let i = oldStartIndex; i <= oldEndIndex; i++) {
      if (oldChildren[i]) {
        const childElement = oldChildren[i].el;
        el.removeChild(childElement);
      }
    }
  }
}